/**
 ** This file is part of FK Klistret Extensions. FK Klistret Extensions 
 ** is free software: you can redistribute it and/or modify it under the 
 ** terms of the GNU General Public License as published by the Free 
 ** Software Foundation, either version 3 of the License, or (at your option) 
 ** any later version.

 ** FK Klistret Extensions is distributed in the hope that it will be 
 ** useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 ** General Public License for more details. You should have received a
 ** copy of the GNU General Public License along with FK Klistret Extensions. 
 ** If not, see <http://www.gnu.org/licenses/>
 */
package se.fk.klistret.migrate.service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.JAXBException;

import org.hibernate.StaleStateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import se.fk.klistret.migrate.exceptions.ApplicationException;
import se.fk.klistret.migrate.utility.FileHelper;

import com.klistret.cmdb.ci.commons.Origin;
import com.klistret.cmdb.ci.commons.Property;
import com.klistret.cmdb.ci.element.component.Software;
import com.klistret.cmdb.ci.element.context.Organization;
import com.klistret.cmdb.ci.element.system.Application;
import com.klistret.cmdb.ci.pojo.Element;
import com.klistret.cmdb.ci.pojo.ElementType;
import com.klistret.cmdb.ci.pojo.Relation;
import com.klistret.cmdb.ci.pojo.RelationType;
import com.klistret.cmdb.ci.relation.Composition;
import com.klistret.cmdb.ci.relation.Dependency;

/**
 * 
 * @author Matthew Young
 * 
 */
public class SoftwareServiceImpl extends BaseElementService implements
		SoftwareService {
	private static final Logger logger = LoggerFactory
			.getLogger(SoftwareServiceImpl.class);

	protected static final Pattern versionNamePattern = Pattern
			.compile("([A-Z0-9]{3,4})_([0-9]{4}_[A-Z0-9]{3,4}+)(_([A-Z0-9]+)){0,1}");

	/**
	 * Software type
	 */
	protected ElementType softwareType;

	/**
	 * Organization list
	 */
	protected List<String> organizations = new ArrayList<String>();

	/**
	 * 
	 * @param name
	 */
	private void doOrganization(String name) {
		if (!organizations.contains(name)) {
			Integer count = elementService
					.count(Arrays.asList(new String[] { String
							.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[pojo:name eq \"%s\" and empty(pojo:toTimeStamp)][pojo:type/pojo:name = \"{http://www.klistret.com/cmdb/ci/element/context}Organization\"]",
									name) }));

			if (count == 0) {
				ElementType elementType = elementTypeService
						.get("{http://www.klistret.com/cmdb/ci/element/context}Organization");

				Element element = new Element();
				element.setType(elementType);
				element.setName(name);
				element.setFromTimeStamp(new Date());
				element.setToTimeStamp(null);
				element.setCreateTimeStamp(new Date());
				element.setUpdateTimeStamp(new Date());

				Organization configuration = new Organization();
				configuration.setName(name);

				element.setConfiguration(configuration);

				elementService.create(element);
			}

			if (count > 1)
				logger.error("Organization [name: {}] exists {} times", name,
						count);
		}
		organizations.add(name);
	}

	/**
	 * A software context has to exist otherwise the entire process is haulted
	 * 
	 * @param name
	 * @return
	 */
	protected boolean isSoftwareContext(String name) {
		if (name == null)
			return false;

		Integer count = elementService
				.count(Arrays.asList(new String[] { String
						.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[pojo:name eq \"%s\" and empty(pojo:toTimeStamp)][pojo:type/pojo:name = \"{http://www.klistret.com/cmdb/ci/element/context}Software\"]",
								name) }));

		if (count == 1)
			return true;

		if (count > 1)
			logger.error("Software (context) [name: {}] exists {} times", name,
					count);

		return false;
	}

	/**
	 * Underlying migrate method (part of SoftwareElementService)
	 */
	protected Element unmarshall(String line, String directory)
			throws JAXBException {
		String[] values = line.split(",");

		String originId = values[0];
		String label = FileHelper.removeQuotes(values[1]);
		Date fromTimeStamp = FileHelper.parseDate(FileHelper
				.removeQuotes(values[2]));
		Date toTimeStamp = FileHelper.parseDate(FileHelper
				.removeQuotes(values[3]));
		Date createTimeStamp = FileHelper.parseDate(FileHelper
				.removeQuotes(values[4]));
		Date updateTimeStamp = FileHelper.parseDate(FileHelper
				.removeQuotes(values[5]));

		String name = null;
		String version = null;
		String trailing = null;

		Matcher m = versionNamePattern.matcher(label);
		while (m.find()) {
			// Name (software context)
			name = m.group(1);

			// Version
			version = m.group(2);

			// Trailing
			if (m.group(3) != null)
				trailing = m.group(3).substring(1, m.group(3).length());
		}

		if (name == null)
			logger.warn("Software (version) name [{}] could not be parsed",
					label);

		Element element = new Element();
		element.setType(getSoftwareType());
		element.setName(name == null ? label : name);
		element.setFromTimeStamp(fromTimeStamp);
		element.setToTimeStamp(toTimeStamp);
		element.setCreateTimeStamp(createTimeStamp);
		element.setUpdateTimeStamp(updateTimeStamp);

		Software configuration = new Software();
		configuration.setName(element.getName());

		Origin origin = new Origin();
		origin.setName("Matrix");
		origin.setIdentification(originId);
		configuration.setOrigin(origin);

		// Organization (default)
		configuration.setOrganization("se.fk");
		doOrganization("se.fk");

		// Software context
		if (!isSoftwareContext(name)) {
			logger.warn("Software (context) [name: {}] not registered", name);
			configuration.getTag().add("!Software Context");
		} else {
			Property property = new Property();
			property.setName("software.context.ancestor");
			property.setValue(name);
			configuration.getProperty().add(property);
		}

		// Software version
		if (version == null) {
			logger.warn("Version not parsed [label: {}]", label);
			configuration.getTag().add("!Version");
			configuration.setVersion("Unknown");
		} else {
			configuration.setVersion(version);
		}

		// Trailing
		if (trailing != null) {
			Property property = new Property();
			property.setName("version.suffix");
			property.setValue(trailing);
			configuration.getProperty().add(property);
		}

		// Original label
		configuration.setLabel(label);

		element.setConfiguration(configuration);
		return element;
	}

	/**
	 * Update software element with availability. Updates based on origin only.
	 * 
	 */
	public void setAvailability(String directory, String fileName,
			String encoding) {
		int lineCount = 0;
		int serviceCount = 0;
		int staleStateCount = 0;

		logger.info("Using directory [{}] and file name [{}]", directory,
				fileName);

		try {
			BufferedReader br = new BufferedReader(new InputStreamReader(
					new FileInputStream(new File(directory, fileName)),
					encoding));

			String line = null;
			while ((line = br.readLine()) != null) {
				try {
					String[] values = line.split(",");

					String timeframeId = values[0];
					String timeframeParentId = values[1];
					String softwareId = values[2];

					Element element = elementService
							.unique(Arrays.asList(new String[] { String
									.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; /pojo:Element[pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/component}Software\"]/pojo:configuration/commons:Origin[@Name eq \"Matrix\" and commons:Identification eq \"%s\"]",
											softwareId) }));

					if (element != null) {
						String originId = timeframeParentId.equals("") ? timeframeId
								: timeframeParentId;

						Element timeframe = elementService
								.unique(Arrays.asList(new String[] { String
										.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; /pojo:Element[pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/context}Timeframe\"]/pojo:configuration/commons:Origin[@Name eq \"Matrix\" and commons:Identification eq \"%s\"]",
												originId) }));

						if (timeframe == null)
							throw new ApplicationException(
									String.format(
											"Timeframe with origin [id: %s] is not registered.",
											originId));

						((Software) element.getConfiguration())
								.setAvailability(timeframe.getName());

						elementService.update(element);
						serviceCount++;
					} else {
						throw new ApplicationException(
								String.format(
										"Software with origin [id: %s] is not registered.",
										softwareId));
					}
				} catch (ApplicationException e) {
					logger.error(String.format(
							"Continue despite application error; %s",
							e.getMessage()));
				} catch (com.klistret.cmdb.exception.NonUniqueObjectException e) {
					if (e.getExpressions() != null) {
						logger.error("Unable perform unique call with the following expressions:");
						for (String expression : e.getExpressions())
							logger.error(expression);
					}
					throw e;
				} catch (com.klistret.cmdb.exception.ApplicationException e) {
					if (e.contains(StaleStateException.class)) {
						staleStateCount++;
						logger.error(e.getMessage());
					} else
						throw e;
				}

				lineCount++;
			}
		} catch (FileNotFoundException e) {
			logger.error("Unable to locate file: {}", e.getMessage());
		} catch (IOException e) {
			logger.error("Unable to handle file: {}", e.getMessage());
		} catch (Exception e) {
			logger.error("Unknown error: {}", e.getMessage());
		} finally {
			if (serviceCount != lineCount)
				logger.warn(
						"Difference between line count [{}] and service count [{}] with file [{}]",
						new Object[] { lineCount, serviceCount, fileName });

			logger.info("Completed {} lines from file [{}], stales [{}]",
					new Object[] { lineCount, fileName, staleStateCount });
		}
	}

	/**
	 * Update software element with environment. Updates based on origin only
	 * and potentially create an application system to bridge the enviroment to
	 * installed software.
	 * 
	 */
	public void setEnvironment(String directory, String fileName,
			String encoding) {
		int lineCount = 0;
		int serviceCount = 0;
		int staleStateCount = 0;

		logger.info("Using directory [{}] and file name [{}]", directory,
				fileName);

		ElementType applicationType = elementTypeService
				.get("{http://www.klistret.com/cmdb/ci/element/system}Application");

		RelationType compositionType = relationTypeService
				.get("{http://www.klistret.com/cmdb/ci/relation}Composition");

		try {
			BufferedReader br = new BufferedReader(new InputStreamReader(
					new FileInputStream(new File(directory, fileName)),
					encoding));

			String line = null;
			while ((line = br.readLine()) != null) {
				try {
					String[] values = line.split(",");

					String environmentId = values[0];
					String softwareId = values[1];
					String applicationId = values[2];
					Date fromTimeStamp = FileHelper.parseDate(FileHelper
							.removeQuotes(values[3]));
					Date toTimeStamp = FileHelper.parseDate(FileHelper
							.removeQuotes(values[4]));
					Date createTimeStamp = FileHelper.parseDate(FileHelper
							.removeQuotes(values[5]));
					Date updateTimeStamp = FileHelper.parseDate(FileHelper
							.removeQuotes(values[6]));

					Element environment = elementService
							.unique(Arrays.asList(new String[] { String
									.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; /pojo:Element[pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/context}Environment\"]/pojo:configuration/commons:Origin[@Name eq \"Matrix\" and commons:Identification eq \"%s\"]",
											environmentId) }));
					if (environment == null)
						throw new ApplicationException(
								String.format(
										"Environment with origin [id: %s] is not registered.",
										environmentId));

					Element software = elementService
							.unique(Arrays.asList(new String[] { String
									.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; /pojo:Element[pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/component}Software\"]/pojo:configuration/commons:Origin[@Name eq \"Matrix\" and commons:Identification eq \"%s\"]",
											softwareId) }));

					if (software == null)
						throw new ApplicationException(
								String.format(
										"Software with origin [id: %s] is not registered.",
										softwareId));

					Element application = elementService
							.unique(Arrays.asList(new String[] { String
									.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; /pojo:Element[pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/system}Application\"]/pojo:configuration/commons:Origin[@Name eq \"Matrix\" and commons:Identification eq \"%s\"]",
											applicationId) }));

					if (application != null) {
						logger.debug("Updating application [id: {}, name: {}]",
								application.getId(), application.getName());
						application.setFromTimeStamp(fromTimeStamp);
						application.setToTimeStamp(toTimeStamp);
						application.setCreateTimeStamp(createTimeStamp);
						application.setUpdateTimeStamp(updateTimeStamp);

						Application configuration = (Application) application
								.getConfiguration();

						/**
						 * Add environment name only if it doesn't exist already
						 */
						if (!configuration.getEnvironment().contains(
								environment.getName()))
							configuration.getEnvironment().add(
									environment.getName());

						application.setConfiguration(configuration);
						application = elementService.update(application);
						serviceCount++;
					} else {
						logger.debug(
								"Creating application [name: {}, environment: {}]",
								software.getName(), environment.getName());
						application = new Element();
						application.setName(software.getName());
						application.setType(applicationType);
						application.setFromTimeStamp(fromTimeStamp);
						application.setToTimeStamp(toTimeStamp);
						application.setCreateTimeStamp(createTimeStamp);
						application.setUpdateTimeStamp(updateTimeStamp);

						Application configuration = new Application();
						configuration.setName(software.getName());
						configuration.getEnvironment().add(
								environment.getName());
						configuration.setState("Unknown");

						Origin origin = new Origin();
						origin.setName("Matrix");
						origin.setIdentification(applicationId);
						configuration.setOrigin(origin);

						application.setConfiguration(configuration);
						application = elementService.create(application);
						serviceCount++;
					}

					/**
					 * Handle relation
					 */
					Relation composition = relationService
							.unique(Arrays.asList(new String[] { String
									.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; /pojo:Relation[pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/relation}Composition\"]/pojo:configuration/commons:Origin[@Name eq \"Matrix\" and commons:Identification eq \"%s\"]",
											applicationId) }));

					if (composition != null) {
						logger.debug(
								"Updating composition relation for application [id: {}, name: {}] and software [id: {}, name: {}]",
								new Object[] { application.getId(),
										application.getName(),
										software.getId(), software.getName() });
						composition.setFromTimeStamp(fromTimeStamp);
						composition.setToTimeStamp(toTimeStamp);
						composition.setCreateTimeStamp(createTimeStamp);
						composition.setUpdateTimeStamp(updateTimeStamp);

						composition.setSource(application);
						composition.setDestination(software);

						Composition configuration = (Composition) composition
								.getConfiguration();

						configuration.setName(software.getName());

						composition.setConfiguration(configuration);

						try {
							relationService.update(composition);
						} catch (com.klistret.cmdb.exception.ApplicationException e) {
							if (e.contains(StaleStateException.class)) {
								staleStateCount++;
								throw new ApplicationException(String.format(
										"Stale state for relation [id: %s]",
										composition.getId()));
							} else
								throw e;
						}
					} else {
						logger.debug(
								"Creating composition relation for application [id: {}, name: {}] and software [id: {}, name: {}]",
								new Object[] { application.getId(),
										application.getName(),
										software.getId(), software.getName() });
						composition = new Relation();
						composition.setType(compositionType);
						composition.setFromTimeStamp(fromTimeStamp);
						composition.setToTimeStamp(toTimeStamp);
						composition.setCreateTimeStamp(createTimeStamp);
						composition.setUpdateTimeStamp(updateTimeStamp);

						composition.setSource(application);
						composition.setDestination(software);

						Composition configuration = new Composition();
						configuration.setName(software.getName());

						Origin origin = new Origin();
						origin.setName("Matrix");
						origin.setIdentification(applicationId);
						configuration.setOrigin(origin);

						composition.setConfiguration(configuration);
						relationService.create(composition);
					}

				} catch (ApplicationException e) {
					logger.error(String.format(
							"Continue despite application error; %s",
							e.getMessage()));
				} catch (com.klistret.cmdb.exception.NonUniqueObjectException e) {
					if (e.getExpressions() != null) {
						logger.error("Unable perform unique call with the following expressions:");
						for (String expression : e.getExpressions())
							logger.error(expression);
					}
					throw e;
				} catch (com.klistret.cmdb.exception.ApplicationException e) {
					if (e.contains(StaleStateException.class)) {
						staleStateCount++;
						logger.error(e.getMessage());
					} else
						throw e;
				}

				lineCount++;
			}
		} catch (FileNotFoundException e) {
			logger.error("Unable to locate file: {}", e.getMessage());
		} catch (IOException e) {
			logger.error("Unable to handle file: {}", e.getMessage());
		} catch (Exception e) {
			logger.error("Unknown exception: {}", e.getMessage());
		} finally {
			if (serviceCount != lineCount)
				logger.warn(
						"Difference between line count [{}] and service count [{}] with file [{}]",
						new Object[] { lineCount, serviceCount, fileName });

			logger.info("Completed {} lines from file [{}], stales [{}]",
					new Object[] { lineCount, fileName, staleStateCount });
		}
	}

	/**
	 * Migrate software/publication dependencies
	 */
	public void migrateDependency(String directory, String fileName,
			String encoding) {
		int lineCount = 0;
		int serviceCount = 0;
		int staleStateCount = 0;

		logger.info("Using directory [{}] and file name [{}]", directory,
				fileName);

		RelationType dependencyType = relationTypeService
				.get("{http://www.klistret.com/cmdb/ci/relation}Dependency");

		try {
			BufferedReader br = new BufferedReader(new InputStreamReader(
					new FileInputStream(new File(directory, fileName)),
					encoding));

			String line = null;
			while ((line = br.readLine()) != null) {
				try {
					String[] values = line.split(",");

					String versionId = values[0];
					String archiveId = values[1];
					String dependencyId = values[2];
					Date fromTimeStamp = FileHelper.parseDate(FileHelper
							.removeQuotes(values[3]));
					Date toTimeStamp = FileHelper.parseDate(FileHelper
							.removeQuotes(values[4]));
					Date createTimeStamp = FileHelper.parseDate(FileHelper
							.removeQuotes(values[5]));
					Date updateTimeStamp = FileHelper.parseDate(FileHelper
							.removeQuotes(values[6]));

					Element software = elementService
							.unique(Arrays.asList(new String[] { String
									.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; /pojo:Element[pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/component}Software\"]/pojo:configuration/commons:Origin[@Name eq \"Matrix\" and commons:Identification eq \"%s\"]",
											versionId) }));

					if (software == null)
						throw new ApplicationException(
								String.format(
										"Software (version) with origin [id: %s] is not registered.",
										versionId));

					Element publication = elementService
							.unique(Arrays.asList(new String[] { String
									.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; /pojo:Element[pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/component}Publication\"]/pojo:configuration/commons:Origin[@Name eq \"Matrix\" and commons:Identification eq \"%s\"]",
											archiveId) }));

					if (publication == null)
						throw new ApplicationException(
								String.format(
										"Publication (tar file) with origin [id: %s] is not registered.",
										archiveId));

					Relation dependency = relationService
							.unique(Arrays.asList(new String[] { String
									.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace commons=\"http://www.klistret.com/cmdb/ci/commons\"; /pojo:Relation[pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/relation}Dependency\"]/pojo:configuration/commons:Origin[@Name eq \"Matrix\" and commons:Identification eq \"%s\"]",
											dependencyId) }));

					if (dependency != null) {
						dependency.setFromTimeStamp(fromTimeStamp);
						dependency.setToTimeStamp(toTimeStamp);
						dependency.setCreateTimeStamp(createTimeStamp);
						dependency.setUpdateTimeStamp(updateTimeStamp);

						dependency.setSource(software);
						dependency.setDestination(publication);

						Dependency configuration = (Dependency) dependency
								.getConfiguration();

						configuration.setName(String.format("%s, %s",
								software.getName(), publication.getName()));

						dependency.setConfiguration(configuration);

						relationService.update(dependency);
						serviceCount++;
					} else {
						dependency = new Relation();
						dependency.setType(dependencyType);
						dependency.setFromTimeStamp(fromTimeStamp);
						dependency.setToTimeStamp(toTimeStamp);
						dependency.setCreateTimeStamp(createTimeStamp);
						dependency.setUpdateTimeStamp(updateTimeStamp);

						dependency.setSource(software);
						dependency.setDestination(publication);

						Dependency configuration = new Dependency();
						configuration.setName(String.format("%s, %s",
								software.getName(), publication.getName()));

						Origin origin = new Origin();
						origin.setName("Matrix");
						origin.setIdentification(dependencyId);
						configuration.setOrigin(origin);

						dependency.setConfiguration(configuration);
						relationService.create(dependency);
						serviceCount++;
					}

				} catch (ApplicationException e) {
					logger.error(String.format(
							"Continue despite application error; %s",
							e.getMessage()));
				} catch (com.klistret.cmdb.exception.NonUniqueObjectException e) {
					if (e.getExpressions() != null) {
						logger.error("Unable perform unique call with the following expressions:");
						for (String expression : e.getExpressions())
							logger.error(expression);
					}
					throw e;
				} catch (com.klistret.cmdb.exception.ApplicationException e) {
					if (e.contains(StaleStateException.class)) {
						staleStateCount++;
						logger.error(e.getMessage());
					} else
						throw e;
				}

				lineCount++;
			}
		} catch (FileNotFoundException e) {
			logger.error("Unable to locate file: {}", e.getMessage());
		} catch (IOException e) {
			logger.error("Unable to handle file: {}", e.getMessage());
		} catch (Exception e) {
			logger.error("Unknown errer: {}", e.getMessage());
		} finally {
			if (serviceCount != lineCount)
				logger.warn(
						"Difference between line count [{}] and service count [{}] with file [{}]",
						new Object[] { lineCount, serviceCount, fileName });
			logger.info("Completed {} lines from file [{}], stales [{}]",
					new Object[] { lineCount, fileName, staleStateCount });
		}
	}

	/**
	 * Get element type
	 */
	protected ElementType getSoftwareType() {
		if (softwareType == null)
			softwareType = elementTypeService
					.get("{http://www.klistret.com/cmdb/ci/element/component}Software");

		return softwareType;
	}
}
